package com.imon.web.codesphere;

import java.net.UnknownHostException;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.stereotype.Component;

import com.google.code.morphia.Datastore;
import com.google.code.morphia.Key;
import com.google.code.morphia.Morphia;
import com.google.code.morphia.annotations.Entity;
import com.google.code.morphia.query.Query;
import com.mongodb.Mongo;
import com.mongodb.MongoException;

@Component
public class MongoDatabase {
	private static final Log LOG = LogFactory.getLog(MongoDatabase.class);

	private Mongo mongo = null;
	private Datastore datastore = null;

	@Value("${mongo.host:\"127.0.0.1\"}")
	private String host;

	@Value("${mongo.port:27017}")
	private int port;

	@Value("${mongo.db:\"codesphere\"}")
	private String database;

	@Value("${base.scan.package}")
	private String baseScanPackage;

	private Morphia morphia;

	public void setBaseScanPackage(final String aPackage) {
		this.baseScanPackage = aPackage;
	}

	public void setDatabase(final String aDatabase) {
		this.database = aDatabase;
	}

	public String getDatabase() {
		return database;
	}

	public void setPort(final int aPort) {
		this.port = aPort;
	}

	public int getPort() {
		return port;
	}

	public void setHost(final String aHost) {
		this.host = aHost;
	}

	public String getHost() {
		return host;
	}

	@PostConstruct
	public void init() {
		LOG.debug(String.format("init()"));
		if (getHost() == null) {
			setHost("127.0.0.1");
			setPort(27017);
			setDatabase("codesphere");
		}

		init(getHost(), getPort(), getDatabase());
	}

	public void init(final String aHost, final int aPort, final String aDatabase) {
		LOG.debug(String.format("init(host=%s, port=%d, database=%s)", aHost, aPort, aDatabase));
		try {
			setupDatabase(aHost, aPort, aDatabase);
		} catch (final UnknownHostException e) {
			throw new DataConnectionException(e);
		} catch (final MongoException e) {
			throw new DataConnectionException(e);
		}
	}

	private void setupDatabase(final String aHost, final int aPort, final String aDatabase) throws UnknownHostException {
		mongo = new Mongo(aHost, aPort);
		morphia = new Morphia();

		// final map all final classes with @Entity
		if (StringUtils.isEmpty(baseScanPackage)) {
			baseScanPackage = this.getClass().getPackage().getName();
			LOG.info("baseScanPackage is not setting therefore using:" + baseScanPackage);
		}

		final ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(
				false);
		provider.addIncludeFilter(new AnnotationTypeFilter(Entity.class));

		for (final BeanDefinition bean : provider.findCandidateComponents(baseScanPackage)) {
			try {
				morphia.map(Class.forName(bean.getBeanClassName()));
			} catch (final ClassNotFoundException e) {
				LOG.error("Error:", e);
			}
		}

		datastore = morphia.createDatastore(mongo, aDatabase);
	}

	@PreDestroy
	public void close() {
		LOG.debug("Closing mongodb connection");
		mongo.close();
	}

	public <T> Key<T> save(final T entity) {
		LOG.debug(String.format("Saving (%s)", entity));
		try {
			return datastore.save(entity);
		} catch (final IllegalArgumentException e) {
			LOG.error("Unable to save entity:" + entity, e);
		}
		return null;
	}

	public <T> void saveAll(final Collection<T> entities) {
		LOG.debug(String.format("saveAll(%s)", entities.size()));
		for (final T entity : entities) {
			save(entity);
		}
	}

	public <T> Collection<T> loadAll(final Class<T> clazz) {
		LOG.debug(String.format("loadAll(%s)", clazz));
		return loadAll(clazz, StringUtils.EMPTY);
	}

	public <T> Collection<T> loadAll(final Class<T> clazz, final String sortedBy) {
		LOG.debug(String.format("loadAll(%s, sortedBy=%s)", clazz, sortedBy));
		if (datastore == null) {
			LOG.error("datastore is null");
			return Collections.emptyList();
		}

		if (StringUtils.isEmpty(sortedBy)) {
			return datastore.find(clazz).asList();
		}

		return datastore.find(clazz).order(sortedBy).asList();
	}

	public <T> T loadByPrimaryKey(final Class<T> clazz, final String idValue) {
		LOG.debug(String.format("getClazz(%s)", idValue));
		if (datastore == null) {
			LOG.error("datastore is null");
			return null;
		}

		return datastore.find(clazz).field("_id").equal(idValue).get();
	}

	public void clear(final Class<?> clazz) {
		LOG.debug(String.format("Clearning %s", clazz));
		datastore.delete(datastore.createQuery(clazz));
	}

	public <T> void mapEntity(final Class<T> clazz) {
		LOG.debug(String.format("mapEntity(%s)", clazz));
		morphia.map(clazz);
	}

	public <T> Collection<T> loadAll(final Class<T> clazz, final Map<String, Object> criteria, final String sortedBy,
			final int skip, final int limit) {
		LOG.debug(String.format("load(%s, %s, %d, %d", clazz, sortedBy, skip, limit));

		final Query<T> query = buildQuery(clazz, criteria);
		return query.order(sortedBy).offset(skip).limit(limit).asList();
	}

	public <T> long count(final Class<T> clazz, final String key, final String value) {
		LOG.debug(String.format("count(%s, %s, %s)", clazz, key, value));
		return datastore.find(clazz).filter(key, value).countAll();
	}

	public <T> Collection<T> loadAll(final Class<T> clazz, final String key, final String value) {
		LOG.debug(String.format("loadAll(%s, key=%s, value=%s)", clazz, key, value));
		return datastore.createQuery(clazz).filter(key, value).asList();
	}

	public <T> Collection<T> loadAll(final Class<T> clazz, final Map<String, Object> criteria, final String sortBy,
			final int limit) {
		final Query<T> query = buildQuery(clazz, criteria);
		return query.order(sortBy).limit(limit).asList();
	}

	public <T> Collection<T> loadAll(final Class<T> clazz, final Map<String, Object> criteria, final String sortBy) {
		final Query<T> query = buildQuery(clazz, criteria);
		return query.order(sortBy).asList();
	}

	public <T> T loadSingle(final Class<T> clazz, final Map<String, Object> criteria, final String sortBy) {
		final Query<T> query = buildQuery(clazz, criteria);
		return query.order(sortBy).get();
	}

	public <T> T loadSingle(final Class<T> clazz, final Map<String, Object> criteria) {
		final Query<T> query = buildQuery(clazz, criteria);
		return query.get();
	}

	public <T> Collection<T> loadAll(final Class<T> clazz, final Map<String, Object> criteria) {
		final Query<T> query = buildQuery(clazz, criteria);
		return query.asList();
	}

	public <T> Collection<T> retrieveFields(final Class<T> clazz, final Map<String, Object> criteria,
			final String sortBy, final String... fields) {
		final Query<T> query = buildQuery(clazz, criteria);
		return query.order(sortBy).retrievedFields(true, fields).asList();
	}

	private <T> Query<T> buildQuery(final Class<T> clazz, final Map<String, Object> criteria) {
		final Query<T> query = datastore.createQuery(clazz);

		for (final String condition : criteria.keySet()) {
			query.filter(condition, criteria.get(condition));
		}

		return query;
	}
}
